/*
 * Copyright (c) 2005-2009 Flamingo Kirill Grouchnikov. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 * 
 *  o Redistributions of source code must retain the above copyright notice, 
 *    this list of conditions and the following disclaimer. 
 *     
 *  o Redistributions in binary form must reproduce the above copyright notice, 
 *    this list of conditions and the following disclaimer in the documentation 
 *    and/or other materials provided with the distribution. 
 *     
 *  o Neither the name of Flamingo Kirill Grouchnikov nor the names of 
 *    its contributors may be used to endorse or promote products derived 
 *    from this software without specific prior written permission. 
 *     
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */
package common;

import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.accessibility.AccessibleContext;
import javax.swing.ButtonModel;
import javax.swing.JComponent;
import javax.swing.SwingConstants;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.EventListenerList;

import common.icon.ResizableIcon;
import common.ui.CommandButtonUI;

/**
 * Base class for command buttons.
 * 
 * @author Kirill Grouchnikov
 */
public abstract class AbstractCommandButton extends JComponent {
    /**
     * Associated icon.
     * 
     * @see #setIcon(ResizableIcon)
     * @see #getIcon()
     */
    protected ResizableIcon icon;

    /**
     * Associated disabled icon.
     * 
     * @see #setDisabledIcon(ResizableIcon)
     * @see #getDisabledIcon()
     */
    protected ResizableIcon disabledIcon;

    /**
     * The button text.
     * 
     * @see #setText(String)
     * @see #getText()
     */
    private String text;

    /**
     * The button action model.
     * 
     * @see #getActionModel()
     * @see #setActionModel(ActionButtonModel)
     */
    protected ButtonModel buttonModel;

    /**
     * Additional text. This is shown for {@link CommandButtonDisplayState#TILE} .
     * 
     * @see #setExtraText(String)
     * @see #getExtraText()
     */
    protected String extraText;

    /**
     * Current display state of <code>this</code> button.
     * 
     * @see #setDisplayState(CommandButtonDisplayState)
     * @see #getDisplayState()
     */
    protected CommandButtonDisplayState displayState;

    /**
     * The dimension of the icon of the associated command button in the
     * {@link CommandButtonDisplayState#FIT_TO_ICON} state.
     * 
     * @see #getCustomDimension()
     * @see #updateCustomDimension(int)
     */
    protected int customDimension;

    /**
     * Indication whether this button is flat.
     * 
     * @see #setFlat(boolean)
     * @see #isFlat()
     */
    protected boolean isFlat;

    /**
     * Horizontal alignment of the content.
     * 
     * @see #setHorizontalAlignment(int)
     * @see #getHorizontalAlignment()
     */
    private int horizontalAlignment;

    /**
     * Scale factor for horizontal gaps.
     * 
     * @see #setHGapScaleFactor(double)
     * @see #getHGapScaleFactor()
     */
    private double hgapScaleFactor;

    /**
     * Scale factor for vertical gaps.
     * 
     * @see #setVGapScaleFactor(double)
     * @see #getVGapScaleFactor()
     */
    private double vgapScaleFactor;

    /**
     * Action handler for the button.
     */
    protected ActionHandler actionHandler;

    /**
     * Key tip for the action area.
     * 
     * @see #setActionKeyTip(String)
     * @see #getActionKeyTip()
     */
    protected String actionKeyTip;

    /**
     * Creates a new command button.
     * 
     * @param text
     *            Button title. May contain any number of words.
     * @param icon
     *            Button icon.
     */
    public AbstractCommandButton(String text, ResizableIcon icon) {
        this.icon = icon;
        this.customDimension = -1;
        this.displayState = CommandButtonDisplayState.FIT_TO_ICON;
        this.horizontalAlignment = SwingConstants.CENTER;
        this.actionHandler = new ActionHandler();
        this.isFlat = true;
        this.hgapScaleFactor = 1.0;
        this.vgapScaleFactor = 1.0;
        this.setText(text);
        this.setOpaque(false);
    }

    /**
     * Sets the new UI delegate.
     * 
     * @param ui
     *            New UI delegate.
     */
    public void setUI(CommandButtonUI ui) {
        super.setUI(ui);
    }

    /**
     * Returns the UI delegate for this button.
     * 
     * @return The UI delegate for this button.
     */
    public CommandButtonUI getUI() {
        return (CommandButtonUI) ui;
    }

    /**
     * Sets new display state for <code>this</code> button. Fires a
     * <code>displayState</code> property change event.
     * 
     * @param state
     *            New display state.
     * @see #getDisplayState()
     */
    public void setDisplayState(CommandButtonDisplayState state) {
        CommandButtonDisplayState old = this.displayState;
        this.displayState = state;

        this.firePropertyChange("displayState", old, this.displayState);
    }

    /**
     * Returns the associated icon.
     * 
     * @return The associated icon.
     * @see #getDisabledIcon()
     * @see #setIcon(ResizableIcon)
     */
    public ResizableIcon getIcon() {
        return icon;
    }

    /**
     * Sets new icon for this button. Fires an <code>icon</code> property
     * change event.
     * 
     * @param defaultIcon
     *            New default icon for this button.
     * @see #setDisabledIcon(ResizableIcon)
     * @see #getIcon()
     */
    public void setIcon(ResizableIcon defaultIcon) {
        ResizableIcon oldValue = this.icon;
        this.icon = defaultIcon;

        firePropertyChange("icon", oldValue, defaultIcon);
        if (defaultIcon != oldValue) {
            if (defaultIcon == null || oldValue == null
                    || defaultIcon.getIconWidth() != oldValue.getIconWidth()
                    || defaultIcon.getIconHeight() != oldValue.getIconHeight()) {
                revalidate();
            }
            repaint();
        }
    }

    /**
     * Sets the disabled icon for this button.
     * 
     * @param disabledIcon
     *            Disabled icon for this button.
     * @see #setIcon(ResizableIcon)
     * @see #getDisabledIcon()
     */
    public void setDisabledIcon(ResizableIcon disabledIcon) {
        this.disabledIcon = disabledIcon;
    }

    /**
     * Returns the associated disabled icon.
     * 
     * @return The associated disabled icon.
     * @see #setDisabledIcon(ResizableIcon)
     * @see #getIcon()
     */
    public ResizableIcon getDisabledIcon() {
        return disabledIcon;
    }

    /**
     * Return the current display state of <code>this</code> button.
     * 
     * @return The current display state of <code>this</code> button.
     * @see #setDisplayState(CommandButtonDisplayState)
     */
    public CommandButtonDisplayState getDisplayState() {
        return displayState;
    }

    /**
     * Returns the extra text of this button.
     * 
     * @return Extra text of this button.
     * @see #setExtraText(String)
     */
    public String getExtraText() {
        return this.extraText;
    }

    /**
     * Sets the extra text for this button. Fires an <code>extraText</code>
     * property change event.
     * 
     * @param extraText
     *            Extra text for this button.
     * @see #getExtraText()
     */
    public void setExtraText(String extraText) {
        String oldValue = this.extraText;
        this.extraText = extraText;
        firePropertyChange("extraText", oldValue, extraText);

        if (accessibleContext != null) {
            accessibleContext.firePropertyChange(
                    AccessibleContext.ACCESSIBLE_VISIBLE_DATA_PROPERTY,
                    oldValue, extraText);
        }
        if (extraText == null || oldValue == null
                || !extraText.equals(oldValue)) {
            revalidate();
            repaint();
        }
    }

    /**
     * Returns the text of this button.
     * 
     * @return The text of this button.
     * @see #setText(String)
     */
    public String getText() {
        return this.text;
    }

    /**
     * Sets the new text for this button. Fires a <code>text</code> property
     * change event.
     * 
     * @param text
     *            The new text for this button.
     * @see #getText()
     */
    public void setText(String text) {
        String oldValue = this.text;
        this.text = text;
        firePropertyChange("text", oldValue, text);

        if (accessibleContext != null) {
            accessibleContext.firePropertyChange(
                    AccessibleContext.ACCESSIBLE_VISIBLE_DATA_PROPERTY,
                    oldValue, text);
        }
        if (text == null || oldValue == null || !text.equals(oldValue)) {
            revalidate();
            repaint();
        }
    }

    /**
     * Updates the dimension of the icon of the associated command button in the
     * {@link CommandButtonDisplayState#FIT_TO_ICON} state. Fires a
     * <code>customDimension</code> property change event.
     * 
     * @param dimension
     *            New dimension of the icon of the associated command button in
     *            the {@link CommandButtonDisplayState#FIT_TO_ICON} state.
     * @see #getCustomDimension()
     */
    public void updateCustomDimension(int dimension) {
        if (this.customDimension != dimension) {
            int old = this.customDimension;
            this.customDimension = dimension;
            this.firePropertyChange("customDimension", old,
                    this.customDimension);
        }
    }

    /**
     * Returns the dimension of the icon of the associated command button in the
     * {@link CommandButtonDisplayState#FIT_TO_ICON} state.
     * 
     * @return The dimension of the icon of the associated command button in the
     *         {@link CommandButtonDisplayState#FIT_TO_ICON} state.
     * @see #updateCustomDimension(int)
     */
    public int getCustomDimension() {
        return this.customDimension;
    }

    /**
     * Returns indication whether this button has flat appearance.
     * 
     * @return <code>true</code> if this button has flat appearance,
     *         <code>false</code> otherwise.
     * @see #setFlat(boolean)
     */
    public boolean isFlat() {
        return this.isFlat;
    }

    /**
     * Sets the flat appearance of this button. Fires a <code>flat</code>
     * property change event.
     * 
     * @param isFlat
     *            If <code>true</code>, this button will have flat
     *            appearance, otherwise this button will not have flat
     *            appearance.
     * @see #isFlat()
     */
    public void setFlat(boolean isFlat) {
        boolean old = this.isFlat;
        this.isFlat = isFlat;
        if (old != this.isFlat) {
            this.firePropertyChange("flat", old, this.isFlat);
        }

        if (old != isFlat) {
            repaint();
        }
    }

    /**
     * Returns the action model for this button.
     * 
     * @return The action model for this button.
     * @see #setActionModel(ActionButtonModel)
     */
    public ButtonModel getActionModel() {
        return this.buttonModel;
    }

    /**
     * Sets the new action model for this button. Fires an
     * <code>actionModel</code> property change event.
     * 
     * @param newModel
     *            The new action model for this button.
     * @see #getActionModel()
     */
    public void setActionModel(ButtonModel newModel) {
        ButtonModel oldModel = getActionModel();

        if (oldModel != null) {
            oldModel.removeChangeListener(this.actionHandler);
            oldModel.removeActionListener(this.actionHandler);
        }

        buttonModel = newModel;

        if (newModel != null) {
            newModel.addChangeListener(this.actionHandler);
            newModel.addActionListener(this.actionHandler);
        }

        firePropertyChange("actionModel", oldModel, newModel);
        if (newModel != oldModel) {
            revalidate();
            repaint();
        }
    }

    /**
     * Adds the specified action listener to this button.
     * 
     * @param l
     *            Action listener to add.
     * @see #removeActionListener(ActionListener)
     */
    public void addActionListener(ActionListener l) {
        this.listenerList.add(ActionListener.class, l);
    }

    /**
     * Removes the specified action listener from this button.
     * 
     * @param l
     *            Action listener to remove.
     * @see #addActionListener(ActionListener)
     */
    public void removeActionListener(ActionListener l) {
        this.listenerList.remove(ActionListener.class, l);
    }

    /**
     * Adds the specified change listener to this button.
     * 
     * @param l
     *            Change listener to add.
     * @see #removeChangeListener(ChangeListener)
     */
    public void addChangeListener(ChangeListener l) {
        this.listenerList.add(ChangeListener.class, l);
    }

    /**
     * Removes the specified change listener from this button.
     * 
     * @param l
     *            Change listener to remove.
     * @see #addChangeListener(ChangeListener)
     */
    public void removeChangeListener(ChangeListener l) {
        this.listenerList.remove(ChangeListener.class, l);
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.swing.JComponent#setEnabled(boolean)
     */
    @Override
    public void setEnabled(boolean b) {
        super.setEnabled(b);
        buttonModel.setEnabled(b);
    }

    /**
     * Default action handler for this button.
     * 
     * @author Kirill Grouchnikov
     */
    class ActionHandler implements ActionListener, ChangeListener {
        public void stateChanged(ChangeEvent e) {
            fireStateChanged();
            repaint();
        }

        public void actionPerformed(ActionEvent event) {
            fireActionPerformed(event);
        }
    }

    /**
     * Notifies all listeners that have registered interest for notification on
     * this event type. The event instance is lazily created.
     * 
     * @see EventListenerList
     */
    protected void fireStateChanged() {
        // Guaranteed to return a non-null array
        Object[] listeners = listenerList.getListenerList();
        // Process the listeners last to first, notifying
        // those that are interested in this event
        ChangeEvent ce = new ChangeEvent(this);
        for (int i = listeners.length - 2; i >= 0; i -= 2) {
            if (listeners[i] == ChangeListener.class) {
                // Lazily create the event:
                ((ChangeListener) listeners[i + 1]).stateChanged(ce);
            }
        }
    }

    /**
     * Notifies all listeners that have registered interest for notification on
     * this event type. The event instance is lazily created using the
     * <code>event</code> parameter.
     * 
     * @param event
     *            the <code>ActionEvent</code> object
     * @see EventListenerList
     */
    protected void fireActionPerformed(ActionEvent event) {
        // Guaranteed to return a non-null array
        Object[] listeners = listenerList.getListenerList();
        ActionEvent e = null;
        // Process the listeners last to first, notifying
        // those that are interested in this event
        for (int i = listeners.length - 2; i >= 0; i -= 2) {
            if (listeners[i] == ActionListener.class) {
                // Lazily create the event:
                if (e == null) {
                    String actionCommand = event.getActionCommand();
                    e = new ActionEvent(this, ActionEvent.ACTION_PERFORMED,
                            actionCommand, event.getWhen(), event
                                    .getModifiers());
                }
                ((ActionListener) listeners[i + 1]).actionPerformed(e);
            }
        }
    }

    /**
     * Sets new horizontal alignment for the content of this button. Fires a
     * <code>horizontalAlignment</code> property change event.
     * 
     * @param alignment
     *            New horizontal alignment for the content of this button.
     * @see #getHorizontalAlignment()
     */
    public void setHorizontalAlignment(int alignment) {
        if (alignment == this.horizontalAlignment)
            return;
        int oldValue = this.horizontalAlignment;
        this.horizontalAlignment = alignment;
        firePropertyChange("horizontalAlignment", oldValue,
                this.horizontalAlignment);
        repaint();
    }

    /**
     * Returns the horizontal alignment for the content of this button.
     * 
     * @return The horizontal alignment for the content of this button.
     * @see #setHorizontalAlignment(int)
     */
    public int getHorizontalAlignment() {
        return this.horizontalAlignment;
    }

    /**
     * Sets new horizontal gap scale factor for the content of this button.
     * Fires an <code>hgapScaleFactor</code> property change event.
     * 
     * @param hgapScaleFactor
     *            New horizontal gap scale factor for the content of this
     *            button.
     * @see #getHGapScaleFactor()
     * @see #setVGapScaleFactor(double)
     * @see #setGapScaleFactor(double)
     */
    public void setHGapScaleFactor(double hgapScaleFactor) {
        if (hgapScaleFactor == this.hgapScaleFactor)
            return;
        double oldValue = this.hgapScaleFactor;
        this.hgapScaleFactor = hgapScaleFactor;
        firePropertyChange("hgapScaleFactor", oldValue, this.hgapScaleFactor);
        if (this.hgapScaleFactor != oldValue) {
            revalidate();
            repaint();
        }
    }

    /**
     * Sets new vertical gap scale factor for the content of this button. Fires
     * a <code>vgapScaleFactor</code> property change event.
     * 
     * @param vgapScaleFactor
     *            New vertical gap scale factor for the content of this button.
     * @see #getVGapScaleFactor()
     * @see #setHGapScaleFactor(double)
     * @see #setGapScaleFactor(double)
     */
    public void setVGapScaleFactor(double vgapScaleFactor) {
        if (vgapScaleFactor == this.vgapScaleFactor)
            return;
        double oldValue = this.vgapScaleFactor;
        this.vgapScaleFactor = vgapScaleFactor;
        firePropertyChange("vgapScaleFactor", oldValue, this.vgapScaleFactor);
        if (this.vgapScaleFactor != oldValue) {
            revalidate();
            repaint();
        }
    }

    /**
     * Sets new gap scale factor for the content of this button.
     * 
     * @param gapScaleFactor
     *            New gap scale factor for the content of this button.
     * @see #getHGapScaleFactor()
     * @see #getVGapScaleFactor()
     */
    public void setGapScaleFactor(double gapScaleFactor) {
        setHGapScaleFactor(gapScaleFactor);
        setVGapScaleFactor(gapScaleFactor);
    }

    /**
     * Returns the horizontal gap scale factor for the content of this button.
     * 
     * @return The horizontal gap scale factor for the content of this button.
     * @see #setHGapScaleFactor(double)
     * @see #setGapScaleFactor(double)
     * @see #getVGapScaleFactor()
     */
    public double getHGapScaleFactor() {
        return this.hgapScaleFactor;
    }

    /**
     * Returns the vertical gap scale factor for the content of this button.
     * 
     * @return The vertical gap scale factor for the content of this button.
     * @see #setVGapScaleFactor(double)
     * @see #setGapScaleFactor(double)
     * @see #getHGapScaleFactor()
     */
    public double getVGapScaleFactor() {
        return this.vgapScaleFactor;
    }

    /**
     * Programmatically perform an action "click". This does the same thing as
     * if the user had pressed and released the action area of the button.
     */
    public void doActionClick() {
        Dimension size = getSize();
        ButtonModel actionModel = this.getActionModel();
        actionModel.setArmed(true);
        actionModel.setPressed(true);
        paintImmediately(new Rectangle(0, 0, size.width, size.height));
        try {
            Thread.sleep(100);
        } catch (InterruptedException ie) {
        }
        actionModel.setPressed(false);
        actionModel.setArmed(false);
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.swing.JComponent#setToolTipText(java.lang.String)
     */
    @Override
    public void setToolTipText(String text) {
        throw new UnsupportedOperationException("Use rich tooltip APIs");
    }
    /**
     * Returns the key tip for the action area of this button.
     * 
     * @return The key tip for the action area of this button.
     * @see #setActionKeyTip(String)
     */
    public String getActionKeyTip() {
        return this.actionKeyTip;
    }

    /**
     * Sets the key tip for the action area of this button. Fires an
     * <code>actionKeyTip</code> property change event.
     * 
     * @param actionKeyTip
     *            The key tip for the action area of this button.
     * @see #getActionKeyTip()
     */
    public void setActionKeyTip(String actionKeyTip) {
        String old = this.actionKeyTip;
        this.actionKeyTip = actionKeyTip;
        this.firePropertyChange("actionKeyTip", old, this.actionKeyTip);
    }
}
